Black Friday Sale Upgrade Your Home →

Reducer Composition with Arrays & Objects

Reducer Composition with Arrays

We left off with code for a todos reducer.

The code is somewhat difficult to read because it mixes 2 different concerns: updating the todos array, as well as updating an individual todo item:

JAVASCRIPT
const todos = (state = [], action) => {
switch (action.type) {
case 'ADD_TODO':
return [
...state,
{
id: action.id,
text: action.text,
completed: false
}
];
case 'TOGGLE_TODO':
return state.map(todo => {
if (todo.id !== action.id) {
return todo;
}
return {
...todo,
completed: !todo.completed
};
});
}
}

Every time a function does too many things, it's best to break them up into other functions that each address only one concern.

In this case, "creating and updating a todo" is a separate task to undertake, so we'll bring this code into a new function that has two arguments: the current state, and the action being dispatched.

Note that in this new function that state refers to the individual todo, and not the list of todos.

JAVASCRIPT
const todo = (state, action) => {
switch (action.type) {
case 'ADD_TODO':
return {
id: action.id,
text: action.text,
completed: false
};
case 'TOGGLE_TODO':
if (state.id !== action.id) {
return state;
}
return {
...state,
completed: !state.completed
};
default:
return state;
}
}

Now that we've extracted our todo reducer from our todos reducer, we have to call it for every todo item and assemble the results into an array:

JAVASCRIPT
const todos = (state = [], action) => {
switch (action.type) {
case 'ADD_TODO':
return [
...state,
todo(undefined, action)
];
case 'TOGGLE_TODO':
return state.map(t => todo(t, action));
default:
return state;
}
};

Remember to have a default case where state is returned to avoid odd bugs in the future.

What we've just done is a common Redux practice called reducer composition. Different reducers specify how different parts of the state tree are updated in response to actions. Since reducers are normal JS functions, they can call other reducers to delegate & abstract away updates to the state.

Reducer composition can be applied many times. While there's a single top-level reducer managing the overall state of the app, it's encouraged to have reducers call eachother as needed to manage the state tree.

Reducer Composition with Objects

In the last section, we used reducer composition to manage Todos in an array.

While storing the application's state with just an array may work for small applications, we can use objects to store more information.

For example, we can add a visibility filter to our Todo application. The state of the visibility filter is a simple string representing the current filter. The filter is changed via the SET_VISIBILITY_FILTER action.

JAVASCRIPT
const visibilityFilter = (
state = 'SHOW_ALL',
action
) => {
switch (action.type) {
case 'SET_VISIBILITY_FILTER':
return action.filter;
default:
return state;
}
};

To store this new information, we don't need to change the existing reducers.

We will use reducer composition to create a new reducer that calls existing reducers to manage their parts of the state, then combine the parts into a single state object.

JAVASCRIPT
const todoApp = (state = {}, action) => {
return {
// Call the `todos()` reducer from last section
todos: todos(
state.todos,
action
),
visibilityFilter: visibilityFilter(
state.visibilityFilter,
action
)
};
};

Note that the first time it runs, it will pass undefined as the state to the child reducers because the initial state of the combined reducer is an empty object, so all its fields are undefined. Remember that when we call reducers with undefined that they return their initial states, thus populating the state for the first time.

When an action comes in, it calls the reducers with the parts of the state that they manage & the action then combines the results into the new state object.

Now that we have composed this new todoApp reducer, we will use it to create the store:

JAVASCRIPT
// You've already imported Redux earlier in the app...
const store = createStore(todoApp);

This pattern helps to scale Redux development, since different team members can work on different reducers that work with the same actions, without stepping on eachother's toes.   Previous      Next